/* @flow */

import {
    tip,
    toArray,
    hyphenate,
    formatComponentName,
    invokeWithErrorHandling
} from '../util/index';
import { updateListeners } from '../vdom/helpers/index';

export function initEvents(vm: Component) {
    //
    vm._events = Object.create(null);
    vm._hasHookEvent = false;
    // init parent attached events
    const listeners = vm.$options._parentListeners;
    if (listeners) {
        //获取父组件上附加的事件，将父组件的事件绑定到当前组件上
        updateComponentListeners(vm, listeners);
    }
}

let target: any;

function add(event, fn) {
    target.$on(event, fn);
}

function remove(event, fn) {
    target.$off(event, fn);
}

function createOnceHandler(event, fn) {
    const _target = target;
    return function onceHandler() {
        const res = fn.apply(null, arguments);
        if (res !== null) {
            _target.$off(event, onceHandler);
        }
    };
}

export function updateComponentListeners(vm: Component, listeners: Object, oldListeners: ?Object) {
    target = vm;
    updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm);
    target = undefined;
}

export function eventsMixin(Vue: Class<Component>) {
    const hookRE = /^hook:/;
    // 注册
    // 判断$on的事件是否为数组，可以给多个事件添加同一个事件处理函数。
    Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
        const vm: Component = this;
        if (Array.isArray(event)) {
            for (let i = 0, l = event.length; i < l; i++) {
                vm.$on(event[i], fn);
            }
        } else {
            //_events对象中是否有这个事件？无，则建个数组push进去
            (vm._events[event] || (vm._events[event] = [])).push(fn);
            // optimize hook:event cost by using a boolean flag marked at registration
            // instead of a hash lookup
            if (hookRE.test(event)) {
                vm._hasHookEvent = true;
            }
        }
        return vm;
    };
    // 注册事件、触发一次
    Vue.prototype.$once = function (event: string, fn: Function): Component {
        const vm: Component = this;
        function on() {
            vm.$off(event, on);
            fn.apply(vm, arguments);
        }
        on.fn = fn;
        vm.$on(event, on);
        return vm;
    };
    //取消事件
    Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
        const vm: Component = this;
        // all
        if (!arguments.length) {
            vm._events = Object.create(null);
            return vm;
        }
        // array of events
        if (Array.isArray(event)) {
            for (let i = 0, l = event.length; i < l; i++) {
                vm.$off(event[i], fn);
            }
            return vm;
        }
        // specific event
        const cbs = vm._events[event];
        if (!cbs) {
            return vm;
        }
        if (!fn) {
            vm._events[event] = null;
            return vm;
        }
        // specific handler
        let cb;
        let i = cbs.length;
        while (i--) {
            cb = cbs[i];
            if (cb === fn || cb.fn === fn) {
                cbs.splice(i, 1);
                break;
            }
        }
        return vm;
    };
    //触发事件
    Vue.prototype.$emit = function (event: string): Component {
        const vm: Component = this;
        if (process.env.NODE_ENV !== 'production') {
            const lowerCaseEvent = event.toLowerCase();
            if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
                tip(
                    `Event "${lowerCaseEvent}" is emitted in component ` +
                        `${formatComponentName(
                            vm
                        )} but the handler is registered for "${event}". ` +
                        `Note that HTML attributes are case-insensitive and you cannot use ` +
                        `v-on to listen to camelCase events when using in-DOM templates. ` +
                        `You should probably use "${hyphenate(event)}" instead of "${event}".`
                );
            }
        }
        let cbs = vm._events[event];
        if (cbs) {
            cbs = cbs.length > 1 ? toArray(cbs) : cbs;
            const args = toArray(arguments, 1);
            const info = `event handler for "${event}"`;
            for (let i = 0, l = cbs.length; i < l; i++) {
                invokeWithErrorHandling(cbs[i], vm, args, vm, info);
            }
        }
        return vm;
    };
}
